// The NSpring Framework for .NET
// Copyright (c) 2003, Jeffrey Varszegi
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without 
// modification, are permitted provided that the following conditions are met:
// 
// - Redistributions of source code must retain the above copyright notice, this 
// list of conditions and the following disclaimer.
// 
// - Redistributions in binary form must reproduce the above copyright notice, 
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// 
// - Neither the name of the NSpring project nor the names of its contributors may
// be used to endorse or promote products derived from this software without 
// specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

namespace NSpring.Logging.Loggers {

using System;
using System.Collections;
using System.IO;
using System.Text;

using NSpring.Logging.Common;
using NSpring.Logging.Common.Pattern;
using NSpring.Logging.Common.Pattern.Patterns;
using NSpring.Logging.DataFormatters;
using NSpring.Logging.EventFormatters;
using NSpring.Logging.Exceptions;

/// <summary>
/// <para>
/// A Logger implementation that writes to files.  The file path may be relative or
/// complete, and may be hard-coded or a formatting pattern.  
/// </para>
/// <para>
/// If necessary, the logger will create any directories necessary.  It caches knowledge
/// of known directories to improve performance.
/// </para>
/// <para>
/// Buffering/asynchronous output is highly recommended with this logger, as it will result
/// in greatly improved performance.  Each file is closed between logging operations
/// for safety and usability reasons (to make it easier to read log files currently in use
/// by an application).
/// </para>
/// <para>
/// File output may be "scattered" to multiple files by the use of a formatting
/// pattern in the file path.  This may be used, for example, to implement "rolling" log
/// files, where output is redirected on a periodic (say, hourly) basis.  Performance in scattering mode is still quite good,
/// as the logger holds multiple streams open in buffering mode unless the path 
/// contains often-changing information such as the event ID or any 
/// time-based token value containing the millisecond.  Use of such volatile paths
/// still works just fine, but it results in the opening and closing of a stream for each 
/// event, resulting in slower performance.
/// </para>
/// <para>
/// Archiving is supported through the use of the IsArchivingEnabled,
/// ArchiveDirectoryPath, and MaximumFileSize properties.  If archiving is enabled,
/// any file that reaches the maximum size is moved to the specified directory, named
/// with an extra extension containing timestamp data (in order to differentiate multiple
/// archives of the same file) and an optional extra file extension in order to facilitate
/// easy reading of archives.  The archives are not compressed by default.
/// </para>
/// <para>
/// The event formatter used by default is an instance of PatternEventFormatter, with 
/// the format "{ts}{z}  [{ln:1w}]  {msg}{data}" .  The data formatter is an instance of
/// FlatDataFormatter, with a Prefix property of "\n  ".
/// </para>
/// </summary>
public sealed class FileLogger : Logger {
    private static string[] dateStrings                 = Constants.DateStrings;
    private static string[] millisecondStrings          = Constants.MillisecondStrings;
    private static string[] yearStrings                 = Constants.YearStrings;
    
    private const char PERIOD_CHAR                      = '.';
    private const string BLANK_STRING                   = "";
    private static string NEW_LINE                      = new string(new char[] {((char)13), ((char)10)});

    private string filePath;
    private FilePathPattern pattern;
    private bool isFilePathStatic;
    private bool isDirectoryPathNonStatic;
    private bool isFilePathVolatile;
    
    private bool isArchivingEnabled                     = false;
    private int maximumFileSize                         = 10485760;
    private string archiveDirectoryPath                 = null;
    private string archiveFileExtension                 = ".txt";
    
    private static Hashtable validDirectories           = new Hashtable(100);
    
    private Random random                               = new Random();
    
    private int maximumOpenFileCount                    = 10;
    private StreamWriter[] openStreams;
    private string[] openFilePaths;

    private bool isFileClosingEnabled                   = false;
    private StreamWriter streamWriter;
    private string lastFilePath                         = null;
    private FileInfo file;
    
    /// <summary>
    /// Constructs a new instance.  The file path must be set before the logger is opened
    /// </summary>
    public FileLogger() {
        eventFormatter = new PatternEventFormatter("{ts}{z}  [{ln:1w}]  {msg}{data}");
        eventFormatter.Suffix = "\n";
        FlatDataFormatter fdf = new FlatDataFormatter();
        fdf.Prefix = "  ";
        eventFormatter.DataFormatter = fdf;
        IsBufferingEnabled = true;
        BufferSize = 1000;
        AutoFlushInterval = 10000;
    }
    
    /// <summary>
    /// Constructs a new instance using the specified file path,
    /// which may be a hard-coded value or a pattern
    /// </summary>
    /// <param name="_filePath">The file path to use</param>
    public FileLogger(string _filePath) {
        FilePath = _filePath;
        eventFormatter = new PatternEventFormatter("{ts}{z}  [{ln:1w}]  {msg}{data}");
        eventFormatter.Suffix = "\n";
        FlatDataFormatter fdf = new FlatDataFormatter();
        fdf.Prefix = "  ";
        eventFormatter.DataFormatter = fdf;
        IsBufferingEnabled = true;
        BufferSize = 1000;
        AutoFlushInterval = 10000;
    }
    
    /// <summary>
    /// Constructs a new instance using the specified file path,
    /// which may be a hard-coded value or a pattern, and a pattern used
    /// in formatting Event objects
    /// </summary>
    /// <param name="_filePath">The file path to use</param>
    /// <param name="_outputPattern">The pattern to use when formatting events</param>
    public FileLogger(string _filePath, string _outputPattern) {
        if (_outputPattern == null) {
            throw new ArgumentNullException();
        }
        FilePath = _filePath;
        eventFormatter = new PatternEventFormatter(_outputPattern);
        eventFormatter.Suffix = "\n";
        eventFormatter.DataFormatter = new FlatDataFormatter();
        IsBufferingEnabled = true;
        BufferSize = 1000;
        AutoFlushInterval = 10000;
    }

    public bool IsFileClosingEnabled {
        get {
            return isFileClosingEnabled;
        }
        set {
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("This property cannot be set while the logger is open");
                }
                isFileClosingEnabled = value;
            }
        }
    }
    
    /// <summary>
    /// Gets/sets whether archiving is enabled.
    /// Cannot be set while the logger is open
    /// </summary>
    public bool IsArchivingEnabled {
        get {
            return isArchivingEnabled;
        }
        set {
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("Cannot set this property while the logger is open");
                }
                isArchivingEnabled = value;
            }
        }
    }

    /// <summary>
    /// Gets/sets the maximum file size for archiving purposes.
    /// Cannot be set while the logger is open
    /// </summary>
    public int MaximumFileSize {
        get {
            return maximumFileSize;
        }
        set {
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("Cannot set this property while the logger is open");
                }
                maximumFileSize = Math.Max(5120, value);
            }
        }
    }
    
    /// <summary>
    /// Gets/sets the archive directory path.
    /// Cannot be set while the logger is open
    /// </summary>
    public string ArchiveDirectoryPath {
        get {
            return archiveDirectoryPath;
        }
        set {
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("Cannot set this property while the logger is open");
                }
                value = value.Trim();
                if (value.Length == 0) {
                    archiveDirectoryPath = null;
                }
                else {
                    archiveDirectoryPath = value;
                }
            }
        }
    }
    
    /// <summary>
    /// Gets/sets the optional extra file extension appended to archive-file names.
    /// Cannot be set while the logger is open
    /// </summary>
    public string ArchiveFileExtension {
        get {
            return archiveDirectoryPath;
        }
        set {
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("Cannot set this property while the logger is open");
                }
                if (value == null) {
                    archiveFileExtension = null;
                }
                else {
                    StringBuilder sb = new StringBuilder();
                    for(int x = 0; x < value.Length; x++) {
                        if (Char.IsLetterOrDigit(value, x)) {
                            sb.Append(value[x]);
                        }
                    }
                    value = sb.ToString().ToLower();
                    if (value.Length == 0) {
                        archiveFileExtension = null;
                    }
                    else {
                        archiveFileExtension = "." + value;
                    }
                }
            }
        }
    }
    
    /// <summary>
    /// Gets/sets the maximum number of file streams held open at once by the logger.
    /// This setting has no effect when the logger is not buffering-enabled.
    /// The default is 10.
    /// Cannot be set while the logger is open
    /// </summary>
    public int MaximumOpenFileCount {
        get {
            return maximumOpenFileCount;
        }
        set {
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("Cannot set this property while the logger is open");
                }
                maximumOpenFileCount = Math.Max(1, value);
            }
        }
    }
    
    /// <summary>
    /// Gets/sets the file path used by the logger, which may be a hard-coded
    /// value or a pattern.
    /// Cannot be set while the logger is open
    /// </summary>
    public string FilePath {
        get {
            return filePath;
        }
        set {
            if ((value == null) || value.Trim().Equals(BLANK_STRING)) {
                throw new ArgumentException("Invalid file path");
            }
            lock (monitor) {
                if (IsOpen) {
                    throw new ApplicationException("Cannot set this property while the logger is open");
                }
                filePath = value;
                pattern = new FilePathPattern(filePath);
                isFilePathStatic = pattern.IsFilePathStatic;
                if (isFilePathStatic) {
                    file = new FileInfo(filePath);
                }
                else {
                    file = null;
                }
                isDirectoryPathNonStatic = !pattern.IsDirectoryPathStatic;
                isFilePathVolatile = pattern.IsFilePathVolatile;
            }
        }
    }
    
    protected override void AcquireResources() {
        if (isArchivingEnabled) {
            if (archiveDirectoryPath == null) {
                throw new ApplicationException("Archiving is enabled, but no archive directory path is set-- cannot start");
            }
            DirectoryInfo archiveDirectory = new DirectoryInfo(archiveDirectoryPath);
            if (!archiveDirectory.Exists) {
                archiveDirectory.Create();
            }
            archiveDirectoryPath = archiveDirectory.FullName + Path.DirectorySeparatorChar;
        }
        
        openStreams = new StreamWriter[maximumOpenFileCount];
        openFilePaths = new string[maximumOpenFileCount];
        
        if (pattern.IsDirectoryPathStatic) {
            (new FileInfo(pattern.Format(new Event(Level.Info, "dummy", null, null, null)))).Directory.Create();
        }

        if (isFilePathStatic && (!IsBufferingEnabled)) {
            streamWriter = new StreamWriter(filePath, true, encoding, 4096);
        }
        
    }
    
    protected override void ReleaseResources() {
        if (streamWriter != null) {
            try {
                streamWriter.Close();
            } catch {}
        }
        validDirectories.Clear();
    }
    
    private string CreateArchiveFilePath(string fileName) {
        DateTime timestamp = DateTime.Now;
        StringBuilder sb;
        string adp = archiveDirectoryPath;
        string afe = archiveFileExtension;
        
        if (afe == null) {
            sb = new StringBuilder(adp.Length + fileName.Length + 21);
            sb.Append(adp);
            sb.Append(fileName);
            sb.Append(PERIOD_CHAR);
            sb.Append(yearStrings[timestamp.Year]);
            sb.Append(dateStrings[timestamp.Month]);
            sb.Append(dateStrings[timestamp.Day]);
            sb.Append(dateStrings[timestamp.Hour]);
            sb.Append(dateStrings[timestamp.Minute]);
            sb.Append(dateStrings[timestamp.Second]);        
            sb.Append(millisecondStrings[timestamp.Millisecond]);
            sb.Append(millisecondStrings[random.Next(1000)]);
        }
        else {
            sb = new StringBuilder(adp.Length + fileName.Length + 21 + afe.Length);
            sb.Append(adp);
            sb.Append(fileName);
            sb.Append(PERIOD_CHAR);
            sb.Append(yearStrings[timestamp.Year]);
            sb.Append(dateStrings[timestamp.Month]);
            sb.Append(dateStrings[timestamp.Day]);
            sb.Append(dateStrings[timestamp.Hour]);
            sb.Append(dateStrings[timestamp.Minute]);
            sb.Append(dateStrings[timestamp.Second]);        
            sb.Append(millisecondStrings[timestamp.Millisecond]);
            sb.Append(millisecondStrings[random.Next(1000)]);
            sb.Append(afe);
        }

        return sb.ToString();
    }
    
    private void ArchiveFile(string filePath) {
        FileInfo f = new FileInfo(filePath);
        if (f.Length > maximumFileSize) {
            string archiveFilePath = CreateArchiveFilePath(f.Name);
            try {
                f.MoveTo(archiveFilePath);
            } catch {}
        }
    }
    
    private void ValidateDirectory(string filePath) {
        DirectoryInfo directory = (new FileInfo(filePath)).Directory;
        if (pattern.IsDirectoryPathVolatile) {
            directory.Create();
        }
        else {
            string directoryPath = directory.FullName;
            if (!validDirectories.ContainsKey(directoryPath)) {
                directory.Create();
                validDirectories.Add(directoryPath, directoryPath);
            }
        }
    }
    
    
    protected override void Write(NSpring.Logging.Event _event) {
        if (isFilePathStatic) {
            if (isFileClosingEnabled) {
                StreamWriter sw = new StreamWriter(filePath, true, encoding, 256); 
                sw.Write(eventFormatter.Format(_event));
                sw.Close();
                if (isArchivingEnabled) {   ArchiveFile(filePath);   }
            }
            else {
                streamWriter.Write(eventFormatter.Format(_event));
                if (isArchivingEnabled) {
                    streamWriter.Flush();
                    if (file.Length > maximumFileSize) {
                        try { streamWriter.Close(); } catch {}
                        streamWriter = null;
                        string archiveFilePath = CreateArchiveFilePath(file.Name);
                        try {
                            file.MoveTo(archiveFilePath);
                        } catch {}
                        streamWriter = new StreamWriter(filePath, true, encoding, 4096);
                    }
                }
            }
        }
        else if (isFilePathVolatile) {
            string filePath = pattern.Format(_event);
            if (isDirectoryPathNonStatic) {   ValidateDirectory(filePath);    }
            StreamWriter sw = new StreamWriter(filePath, true, encoding, 256); 
            sw.Write(eventFormatter.Format(_event));
            sw.Close();
        }
        else {
            string path = pattern.Format(_event);
            if (isDirectoryPathNonStatic) {   ValidateDirectory(path);    }
            if (isFileClosingEnabled) {
                StreamWriter sw = new StreamWriter(path, true, encoding, 256); 
                sw.Write(eventFormatter.Format(_event));
                sw.Close();
                if (isArchivingEnabled) {   ArchiveFile(path);   }
            }
            else {
                if (streamWriter == null) {
                    streamWriter = new StreamWriter(path, true, encoding, 4096);
                }
                else {
                    if (!path.Equals(lastFilePath)) {
                        streamWriter.Close();
                        if (isArchivingEnabled) {   ArchiveFile(lastFilePath);   }
                        streamWriter = new StreamWriter(path, true, encoding, 4096);
                        lastFilePath = path;
                    }
                }
                streamWriter.Write(eventFormatter.Format(_event));
            }
        }
    }
    
    private void WriteVolatile(NSpring.Logging.Event _event) {
        string filePath = pattern.Format(_event);
        
        if (isDirectoryPathNonStatic) {   ValidateDirectory(filePath);    }
        
        StreamWriter sw = new StreamWriter(filePath, true, encoding, 256); 
        sw.Write(eventFormatter.Format(_event));
        sw.Close();
    }

    protected override void Write(Event[] events, int length) {
        if (length == 0) {
            return;
        }
        
        if (isFilePathStatic) {
            StreamWriter sw = new StreamWriter(filePath, true, encoding, 8192);
            for(int x = 0; x < length; x++) {
                sw.Write(eventFormatter.Format(events[x]));
            }
            sw.Close();
        }
        else {

            if (isFilePathVolatile) {
                for(int x = 0; x < length; x++) {
                    try {
                        Write(events[x]);
                    } catch (Exception ex) {
                        ExceptionHandler.Handle(new LoggerException(this, events[x], ex));
                    }
                }
            }
            else {
                string filePath;
                Event e;
                StreamWriter streamWriter;
                int openFileCount = 0;
                bool eventWritten;

                int x, y;
                for(x = 0; x < length; x++) {
                    streamWriter = null;
                    e = events[x];
                    filePath = pattern.Format(e);
                    eventWritten = false;
                    
                    for(y = 0; y < openFileCount; y++) {
                        if (openFilePaths[y].Equals(filePath)) {
                            try {
                                streamWriter = openStreams[y];
                                streamWriter.Write(eventFormatter.Format(e));
                                if (y > 0) {
                                    openStreams[y] = openStreams[y - 1];
                                    openFilePaths[y] = openFilePaths[y - 1];
                                    openStreams[y - 1] = streamWriter;
                                    openFilePaths[y - 1] = filePath;
                                }
                                eventWritten = true;
                            } catch {
                                try { streamWriter.Close(); } catch {}
                                for(int z = y; z < (openFileCount - 1); z++) {
                                    openStreams[z] = openStreams[z + 1];
                                    openFilePaths[z] = openFilePaths[z + 1];
                                }
                                openStreams[openFileCount - 1] = null;
                                openFilePaths[openFileCount - 1] = null;
                                openFileCount--;
                            }
                            break;
                        }
                    }

                    if (!eventWritten) {
                        if (!pattern.IsDirectoryPathStatic) {   ValidateDirectory(filePath);    }

                        try {
                            streamWriter = new StreamWriter(filePath, true, encoding, 8192); 
                            streamWriter.Write(eventFormatter.Format(e));
                        } catch (Exception ex) {
                            try {   streamWriter.Close();   } catch {}
                            ExceptionHandler.Handle(new LoggerException(this, e, ex));
                            continue;
                        }
                        
                        if (openFileCount == maximumOpenFileCount) {
                            try { openStreams[openFileCount - 1].Close(); } catch {}
                            if (isArchivingEnabled) {   ArchiveFile(openFilePaths[openFileCount - 1]);  }
                            for(y = (openFileCount - 1); y > 0; y--) {
                                openStreams[y] = openStreams[y - 1];
                                openFilePaths[y] = openFilePaths[y - 1];
                            }
                            openStreams[0] = streamWriter;
                            openFilePaths[0] = filePath;
                        }
                        else {
                            for(y = openFileCount; y > 0; y--) {
                                openStreams[y] = openStreams[y - 1];
                                openFilePaths[y] = openFilePaths[y - 1];
                            }
                            openStreams[0] = streamWriter;
                            openFilePaths[0] = filePath;
                            openFileCount++;
                        }
                        

                    }
                }
                
                if (isArchivingEnabled) {
                    for(x = 0; x < openFileCount; x++) {
                        try {
                            openStreams[x].Close();
                        } catch {}
                        ArchiveFile(openFilePaths[x]);
                    }
                }
                else {
                    for(x = 0; x < openFileCount; x++) {
                        try {
                            openStreams[x].Close();
                        } catch {}
                    }
                }
                Array.Clear(openStreams, 0, openFileCount);
                Array.Clear(openFilePaths, 0, openFileCount);

            }
            
        }

    }
    
}

}
